Skip to content

fix: evict idle HTTP sessions instead of leaking them - #19

Merged
ConalMullan merged 1 commit into
developfrom
fix/http-session-leak
Aug 11, 2026
Merged

fix: evict idle HTTP sessions instead of leaking them#19
ConalMullan merged 1 commit into
developfrom
fix/http-session-leak

Conversation

@ConalMullan

Copy link
Copy Markdown
Collaborator

Problem

HTTP transport sessions were only removed on an explicit DELETE /mcp or on transport.close(). Most MCP clients (claude.ai, Claude Desktop) send neither — they just stop talking — so every session stayed in the map along with the Server instance behind it.

Observed on the dev host: 449 Session initialized log lines and zero closes of either kind, with activeSessions climbing monotonically since deploy.

initialized:              449
closed (onsessionclosed):   0
closed (onclose):           0

61 MiB resident at the time of diagnosis — not yet harmful, but unbounded.

Fix

New src/session-registry.ts: last-seen tracking per session plus a periodic sweep that closes and evicts anything idle past SESSION_IDLE_TIMEOUT_MS.

Two details that matter more than the sweep itself:

  • Open SSE streams are never swept. A GET /mcp stream can sit quiet for hours; sweeping on last-request-time would kill live connections. The registry counts open streams and restarts the idle clock when the last one closes.
  • The entry is removed before close(). A close() that throws on an already-dead socket would otherwise leave the entry behind, reintroducing the exact leak. Covered by a test.

Config

Variable Default Notes
SESSION_IDLE_TIMEOUT_MS 1800000 (30min) 0 disables sweeping entirely
SESSION_SWEEP_INTERVAL_MS 300000 (5min) how often the sweep runs

Observability

/health now also reports streamingSessions and a cumulative sweptSessions, so after deploy activeSessions should plateau rather than climb.

Testing

15 new unit tests covering eviction, the within-timeout and recent-traffic cases, open/multiple/closed streams, failing close(), disabled sweeping, and a regression test that drains 449 abandoned sessions.

540 tests passing (was 525). Lint and format clean.

Sessions were only ever removed on an explicit DELETE /mcp or on
transport.close(). Most MCP clients (claude.ai, Claude Desktop) send
neither - they just stop talking - so every session stayed in the map
along with the Server instance behind it.

On the dev host this showed as 449 "Session initialized" log lines and
zero closes of either kind, with activeSessions climbing monotonically
since deploy.

Add SessionRegistry: last-seen tracking per session plus a periodic
sweep that closes and evicts anything idle past SESSION_IDLE_TIMEOUT_MS
(default 30min, 0 disables). Sessions holding an open SSE stream are
never swept however quiet they are - streaming is activity, and evicting
one would kill a live connection. The sweep removes the entry before
calling close(), so a throwing close() cannot leave the entry behind.

/health now also reports streamingSessions and a cumulative
sweptSessions so the behaviour is observable in production.
@ConalMullan
ConalMullan merged commit 63183c4 into develop Aug 11, 2026
3 checks passed
@ConalMullan
ConalMullan deleted the fix/http-session-leak branch August 11, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant